﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Xml;

using Storm.TextEditor.Editor;
using Storm.TextEditor.Languages;
using Storm.TextEditor.Parser.Objects;
using Storm.TextEditor.Parser.Objects.Collections;

namespace Storm.TextEditor.Parser.XML
{
	/// <summary>
	/// Represents a class that reads language definitions from XML sheets.
	/// </summary>
	public class LanguageReader
	{
		#region Fields

		private Hashtable    styles    = new Hashtable();
		private Hashtable    blocks    = new Hashtable();

		private Language     language        = new Language();
		private LanguageList parsedLanguages = new LanguageList();

		private static bool   useUserCustomStyles = false;
		private static string userCustomStyles    = null;

		#endregion

		#region Properties

		/// <summary>
		/// Gets or sets the custom user styles.
		/// </summary>
		public static string UserCustomStyles
		{
			get { return LanguageReader.userCustomStyles; }
			set { LanguageReader.userCustomStyles = value; }
		}

		/// <summary>
		/// Gets or sets whether to use custom user styles.
		/// </summary>
		public static bool UseUserCustomStyles
		{
			get { return useUserCustomStyles; }
			set { useUserCustomStyles = value; }
		}

		#endregion

		#region Methods

		#region Public

		/// <summary>
		/// Loads the given file.
		/// </summary>
		/// <param name="filename">File to load.</param>
		/// <returns>Language found in the given file.</returns>
		public Language Load(string filename)
		{
			return Load(File.OpenRead(filename));
		}

		/// <summary>
		/// Loads the given file.
		/// </summary>
		/// <param name="stream">File to load.</param>
		/// <returns>Language found in the given file.</returns>
		public Language Load(Stream stream)
		{
			styles = new Hashtable();
			blocks = new Hashtable();
			language = new Language();

			XmlDocument langDocument = new XmlDocument();
			langDocument.Load(stream);

			if (useUserCustomStyles == true && Directory.Exists(userCustomStyles) == true)
			{
				string langName = langDocument.SelectSingleNode("Language/@Name").InnerText;
				string path = Path.Combine(LanguageReader.UserCustomStyles, langName + ".conf");

				if (File.Exists(path) == true)
				{
					XmlDocument userConf = new XmlDocument();
					userConf.Load(path);

					XmlNodeList nodeList = langDocument.SelectNodes("Language/Style");
					foreach (XmlNode current in nodeList)
					{
						XmlNode userStyleNode = userConf.SelectSingleNode("styles/Style[@Name='" +
							current.Attributes["Name"].InnerText + "']");

						if (userStyleNode == null)
							continue;

						foreach (XmlAttribute userAtt in userStyleNode.Attributes)
							current.Attributes[userAtt.LocalName].InnerText = userAtt.InnerText;
					}
				}
			}

			this.ReadLanguageDefinition(langDocument);
			return language;
		}

		/// <summary>
		/// Loads the given file.
		/// </summary>
		/// <param name="filename">File to load.</param>
		/// <param name="separators">Separators to use when reading the file.</param>
		/// <returns>Language found in the given file.</returns>
		public Language Load(string filename, string separators)
		{
			styles = new Hashtable();
			blocks = new Hashtable();

			language = new Language();
			language.Separators = separators;

			XmlDocument document = new XmlDocument();
			document.Load(filename);

			this.ReadLanguageDefinition(document);
			return language;
		}

		/// <summary>
		/// Loads the given XML.
		/// </summary>
		/// <param name="xml">XML to load.</param>
		/// <returns>Language found in the given XML.</returns>
		public Language LoadXml(string xml)
		{
			styles = new Hashtable();
			blocks = new Hashtable();

			language = new Language();

			XmlDocument document = new XmlDocument();
			document.LoadXml(xml);

			this.ReadLanguageDefinition(document);
			return language;
		}

		#endregion

		#region Private

		/// <summary>
		/// Reads the given XmlDocument's first Language XmlNode.
		/// </summary>
		/// <param name="document">Document to read.</param>
		private void ReadLanguageDefinition(XmlDocument document)
		{
			this.ParseLanguage(document["Language"]);
		}

		/// <summary>
		/// Parses the given XmlNode and its children for languages.
		/// </summary>
		/// <param name="node">XmlNode to parse.</param>
		private void ParseLanguage(XmlNode node)
		{
			string name = "";
			string startBlock = "";

			foreach (XmlAttribute att in node.Attributes)
			{
				if (att.Name.ToLower() == "name")
					name = att.Value;

				if (att.Name.ToLower() == "startblock")
					startBlock = att.Value;
			}

			language.Name = name;
			language.MainBlock = this.GetBlock(startBlock);

			foreach (XmlNode childNode in node.ChildNodes)
			{
				if (childNode.NodeType == XmlNodeType.Element)
				{
					switch (childNode.Name.ToLower())
					{
						case "filetypes":
							this.ParseFileTypes(childNode);
							break;
						case "inherits":
							this.ParseInherits(childNode);
							break;
						case "block":
							this.ParseBlock(childNode);
							break;
						case "style":
							this.ParseStyle(childNode);
							break;
					}
				}
			}
		}

		/// <summary>
		/// Parses the given XmlNode and its children for file types.
		/// </summary>
		/// <param name="node">XmlNode to parse.</param>
		private void ParseFileTypes(XmlNode node)
		{
			foreach (XmlNode childNode in node.ChildNodes)
			{
				if (childNode.NodeType == XmlNodeType.Element)
				{
					if (childNode.Name.ToLower() == "filetype")
					{
						string extension = "";
						string name = "";

						foreach (XmlAttribute att in childNode.Attributes)
						{
							switch (att.Name.ToLower())
							{
								case "name":
									name = att.Value;
									break;
								case "extension":
									extension = att.Value;
									break;
							}
						}

						FileType fileType = new FileType();
						fileType.Extension = extension;
						fileType.Name = name;

						language.FileTypes.Add(fileType);
					}
				}
			}
		}

		/// <summary>
		/// Parses the given XmlNode and its children for inheritance.
		/// </summary>
		/// <param name="node">XmlNode to parse.</param>
		private void ParseInherits(XmlNode node)
		{
			foreach (XmlNode childNode in node.ChildNodes)
			{
				if (childNode.NodeType == XmlNodeType.Element)
				{
					if (childNode.Name.ToLower() == "inherit")
					{
						string file = "";

						foreach (XmlAttribute attribute in childNode.Attributes)
						{
							switch (attribute.Name.ToLower())
							{
								case "file":
									file = attribute.Value;
									break;
							}
						}

						Hashtable tempBlocks = blocks;
						Hashtable tempStyles = styles;

						Language tempLanguage = language;
						int localIndex = file.ToLower().IndexOf("local:");
						bool local = localIndex > -1;

						Language localLanguage = null;
						if (local == true)
						{
							string fileName = file.Remove(localIndex, "local:".Length);
							Stream fileStream = typeof(LanguageLoader).Assembly.GetManifestResourceStream("Storm.TextEditor.Languages." + fileName);

							localLanguage = this.Load(fileStream);
						}
						else
						{
							Stream fileStream = File.Open(file, FileMode.Open);
							localLanguage = this.Load(fileStream);
						}

						blocks = tempBlocks;
						styles = tempStyles;

						language = tempLanguage;
						if (localLanguage != null)
						{
                            language.DerivedLanguages.Add(localLanguage);

							foreach (TextStyle style in localLanguage.Styles)
							{
								if (string.IsNullOrEmpty(style.Name) == false)
									styles.Add(style.Name, style);
							}

							foreach (Block block in localLanguage.Blocks)
							{
								if (blocks.ContainsKey(block.Name) == false)
									blocks.Add(block.Name, block);
							}

							foreach (PatternList patternList in localLanguage.MainBlock.KeywordsList)
								language.MainBlock.KeywordsList.Add(patternList);

							foreach (PatternList patternList in localLanguage.MainBlock.OperatorsList)
								language.MainBlock.OperatorsList.Add(patternList);
						}
					}
				}
			}
		}

		/// <summary>
		/// Parses the given XmlNode and its children for blocks.
		/// </summary>
		/// <param name="node">XmlNode to parse.</param>
		private void ParseBlock(XmlNode node)
		{
			string name         = "";
			string style        = "";
			string patternStyle = "";

			bool multiline         = false;
			bool terminateChildren = false;

			Color backColor = Color.Transparent;

			foreach (XmlAttribute att in node.Attributes)
			{
				switch (att.Name.ToLower())
				{
					case "name":
						name = att.Value;
						break;
					case "style":
						style = att.Value;
						break;
					case "patternstyle":
						patternStyle = att.Value;
						break;
					case "ismultiline":
						multiline = bool.Parse(att.Value);
						break;
					case "terminatechildren":
						terminateChildren = bool.Parse(att.Value);
						break;
					case "backcolor":
						backColor = Color.FromName(att.Value);
						break;
				}
			}

			Block blockList = GetBlock(name);
			blockList.BackColor = backColor;
			blockList.Name = name;
			blockList.Multiline = multiline;
			blockList.Style = GetStyle(style);
			blockList.TerminateChildren = terminateChildren;

			foreach (XmlNode childNode in node.ChildNodes)
			{
				if (childNode.NodeType == XmlNodeType.Element)
				{
					if (childNode.Name.ToLower() == "indent")
					{
						string start = "";
						string end = "";
						string nodeStyle = "";

						bool startIsComplex = false;
						bool startIsKeyword = true;
						bool endIsComplex = false;
						bool endIsKeyword = true;

						foreach (XmlAttribute att in childNode.Attributes)
						{
							switch (att.Name.ToLower())
							{
								case "start":
									start = att.Value;
									break;
								case "end":
									end = att.Value;
									break;
								case "startiscomplex":
									startIsComplex = bool.Parse(att.Value);
									break;
								case "startiskeyword":
									startIsKeyword = bool.Parse(att.Value);
									break;
								case "endiscomplex":
									endIsComplex = bool.Parse(att.Value);
									break;
								case "endiskeyword":
									endIsKeyword = bool.Parse(att.Value);
									break;
								case "style":
									nodeStyle = att.Value;
									break;
							}
						}

						if (start != "")
						{
							Scope scope = new Scope();

							scope.CauseIndent = true;
							scope.Indenter = true;
                            scope.DefaultExpanded = true;

							scope.Style = this.GetStyle(nodeStyle);
							scope.ExpansionText = start + "..." + end;
                            scope.NoHighlighting = startIsComplex;

							Pattern startPattern = new Pattern(start, startIsComplex, false, startIsKeyword);
							Pattern endPattern =  new Pattern(end, endIsComplex, false, endIsKeyword);

							scope.Start = startPattern;
							scope.EndPatterns.Add(endPattern);
							blockList.ScopePatterns.Add(scope);
						}
					}

					if (childNode.Name.ToLower() == "scope")
					{
						string start = "";
						string end = "";
						string nodeStyle = "";
						string text = "";

						string startIsSeparator = "";
						string endIsSeparator = "";

						string startIsComplex = "false";
						string endIsComplex = "false";

						string startIsKeyword = "false";
						string endIsKeyword = "false";

						string spawnStart = "";
						string spawnEnd = "";

						string escapeChar = "";
						string causeIndent = "false";

						bool expanded = true;
						bool noHighlighting = false;

						foreach (XmlAttribute att in childNode.Attributes)
						{
							switch (att.Name.ToLower())
							{
								case "start":
									start = att.Value;
									break;
								case "end":
									end = att.Value;
									break;
								case "escapechar":
									escapeChar = att.Value;
									break;
								case "style":
									nodeStyle = att.Value;
									break;
								case "text":
									text = att.Value;
									break;
								case "defaultexpanded":
									expanded = bool.Parse(att.Value);
									break;
								case "endisseparator":
									endIsSeparator = att.Value;
									break;
								case "startisseparator":
									startIsSeparator = att.Value;
									break;
								case "startiskeyword":
									startIsKeyword = att.Value;
									break;
								case "startiscomplex":
									startIsComplex = att.Value;
									noHighlighting = true;
									break;
								case "endiscomplex":
									endIsComplex = att.Value;
									noHighlighting = true;
									break;
								case "endiskeyword":
									endIsKeyword = att.Value;
									break;
								case "spawnblockonstart":
									spawnStart = att.Value;
									break;
								case "spawnblockonend":
									spawnEnd = att.Value;
									break;
								case "causeindent":
									causeIndent = att.Value;
									break;
								case "nohighlighting":
									if (bool.Parse(startIsComplex) == false && bool.Parse(endIsComplex) == false)
										noHighlighting = bool.Parse(att.Value);

									break;
							}
						}

						if (start != "")
						{
							Scope scope = new Scope();
							scope.Style = this.GetStyle(nodeStyle);
							scope.ExpansionText = text;
							scope.DefaultExpanded = expanded;

							bool boolStartIsComplex = bool.Parse(startIsComplex);
							bool boolEndIsComplex   = bool.Parse(endIsComplex);
							bool boolCauseIndent    = bool.Parse(causeIndent);

							scope.CauseIndent = boolCauseIndent;
							scope.NoHighlighting = noHighlighting;

							Pattern startPattern = new Pattern(start, boolStartIsComplex, false, bool.Parse(startIsKeyword));
                            Pattern endPattern   = escapeChar != "" ? new Pattern(end, boolEndIsComplex, false, bool.Parse(endIsKeyword), escapeChar) :
                                new Pattern(end, boolEndIsComplex, false, bool.Parse(endIsKeyword));

							if (endIsSeparator != "")
								endPattern.IsSeparator = bool.Parse(endIsSeparator);

							scope.Start = startPattern;
							scope.EndPatterns.Add(endPattern);
							blockList.ScopePatterns.Add(scope);

							if (spawnStart != "")
								scope.SpawnBlockOnStart = this.GetBlock(spawnStart);

							if (spawnEnd != "")
								scope.SpawnBlockOnEnd = this.GetBlock(spawnEnd);
						}
					}

					if (childNode.Name.ToLower() == "bracket")
					{
						string start      = "";
						string end        = "";
						string childStyle = "";

						string endIsSeparator   = "";
						string startIsSeparator = "";

						string startIsComplex = "false";
						string endIsComplex   = "false";

						string startIsKeyword = "false";
						string endIsKeyword   = "false";
						string childMultiline = "true";

						foreach (XmlAttribute att in childNode.Attributes)
						{
							switch (att.Name.ToLower())
							{
								case "start":
									start = att.Value;
									break;
								case "end":
									end = att.Value;
									break;
								case "style":
									childStyle = att.Value;
									break;
								case "endisseparator":
									endIsSeparator = att.Value;
									break;
								case "startisseparator":
									startIsSeparator = att.Value;
									break;
								case "startiskeyword":
									startIsKeyword = att.Value;
									break;
								case "startiscomplex":
									startIsComplex = att.Value;
									break;
								case "endiscomplex":
									endIsComplex = att.Value;
									break;
								case "ismultiline":
									childMultiline = att.Value;
									break;
							}
						}

						if (start != "")
						{
							PatternList patternList = new PatternList();
							patternList.Style = this.GetStyle(childStyle);

							bool boolStartIsComplex = bool.Parse(startIsComplex);
							bool boolEndIsComplex   = bool.Parse(endIsComplex);
							bool boolMultiline      = bool.Parse(childMultiline);

							Pattern startPattern = new Pattern(start, boolStartIsComplex, false, bool.Parse(startIsKeyword));
							Pattern endPattern   = new Pattern(end, boolEndIsComplex, false, bool.Parse(endIsKeyword));

							startPattern.MatchingBracket = endPattern;
							endPattern.MatchingBracket   = startPattern;

							startPattern.BracketType = BracketType.StartBracket;
							endPattern.BracketType   = BracketType.EndBracket;

							startPattern.IsMultilineBracket = endPattern.IsMultilineBracket = boolMultiline;

							patternList.Add(startPattern);
							patternList.Add(endPattern);
							blockList.OperatorsList.Add(patternList);
						}
					}
				}

				if (childNode.Name.ToLower() == "keywords")
				{
					foreach (XmlNode exChildNode in childNode.ChildNodes)
					{
						if (exChildNode.Name.ToLower() == "patterngroup")
						{
							PatternList patternList = new PatternList();
							blockList.KeywordsList.Add(patternList);

							foreach (XmlAttribute att in exChildNode.Attributes)
							{
								switch (att.Name.ToLower())
								{
									case "name":
										patternList.Name = att.Value;
										break;
									case "style":
										patternList.Style = GetStyle(att.Value);
										break;
									case "normalizecase":
										patternList.NormalizeCase = bool.Parse(att.Value);
										break;
									case "casesensitive":
										patternList.CaseSensitive = bool.Parse(att.Value);
										break;
								}
							}

							foreach (XmlNode moreExChildNode in exChildNode.ChildNodes)
							{
								if (moreExChildNode.Name.ToLower() == "pattern")
								{
									bool complex   = false;
									bool separator = false;

									string category = "";
									string pattern  = "";

									if (moreExChildNode.Attributes != null)
									{
										foreach (XmlAttribute att in moreExChildNode.Attributes)
										{
											switch (att.Name.ToLower())
											{
												case "text":
													pattern = att.Value;
													break;
												case "iscomplex":
													complex = bool.Parse(att.Value);
													break;
												case "isseparator":
													separator = bool.Parse(att.Value);
													break;
												case "category":
													category = (att.Value);
													break;
											}
										}
									}

									if (pattern != "")
									{
										Pattern exPattern = new Pattern(pattern, complex, separator, true);
										exPattern.Category = category;
										patternList.Add(exPattern);
									}
								}
								else if (moreExChildNode.Name.ToLower() == "patterns")
								{
									string patterns = moreExChildNode.ChildNodes[0].Value;
									patterns = patterns.Replace("\t", " ");

									while (patterns.IndexOf("  ") >= 0)
										patterns = patterns.Replace("  ", " ");

									foreach (string pattern in patterns.Split())
									{
										if (pattern != "")
											patternList.Add(new Pattern(pattern, false, false, true));
									}
								}
							}
						}
					}
				}

				if (childNode.Name.ToLower() == "operators")
				{
					foreach (XmlNode exChildNode in childNode.ChildNodes)
					{
						if (exChildNode.Name.ToLower() == "patterngroup")
						{
							PatternList patternList = new PatternList();
							blockList.OperatorsList.Add(patternList);
							foreach (XmlAttribute att in exChildNode.Attributes)
							{
								switch (att.Name.ToLower())
								{
									case "style":
										patternList.Style = GetStyle(att.Value);
										break;
									case "name":
										patternList.Name = att.Value;
										break;
									case "normalizecase":
										patternList.NormalizeCase = bool.Parse(att.Value);
										break;
									case "casesensitive":
										patternList.CaseSensitive = bool.Parse(att.Value);
										break;
								}
							}

							foreach (XmlNode moreExChildNode in exChildNode.ChildNodes)
							{
								if (moreExChildNode.Name.ToLower() == "pattern")
								{
									bool complex   = false;
									bool separator = false;

									string pattern  = "";
									string category = null;

									if (moreExChildNode.Attributes != null)
									{
										foreach (XmlAttribute att in moreExChildNode.Attributes)
										{
											switch (att.Name.ToLower())
											{
												case "text":
													pattern = att.Value;
													break;
												case "iscomplex":
													complex = bool.Parse(att.Value);
													break;
												case "isseparator":
													separator = bool.Parse(att.Value);
													break;
												case "category":
													category = (att.Value);
													break;
											}
										}
									}

									if (pattern != "")
									{
										Pattern exPattern = new Pattern(pattern, complex, separator, false);
										exPattern.Category = category;
										patternList.Add(exPattern);
									}
								}
								else if (moreExChildNode.Name.ToLower() == "patterns")
								{
									string patterns = moreExChildNode.ChildNodes[0].Value;
									patterns = patterns.Replace("\t", " ");

									while (patterns.IndexOf("  ") >= 0)
										patterns = patterns.Replace("  ", " ");

									string[] splittedPatterns = patterns.Split();
									foreach (string pattern in splittedPatterns)
									{
										if (pattern != "")
											patternList.Add(new Pattern(pattern, false, false, false));
									}
								}
							}
						}
					}
				}

				if (childNode.Name.ToLower() == "childblocks")
				{
					foreach (XmlNode exChildNode in childNode.ChildNodes)
					{
						if (exChildNode.Name.ToLower() == "child")
						{
							foreach (XmlAttribute att in exChildNode.Attributes)
							{
								if (att.Name.ToLower() == "name")
									blockList.ChildBlocks.Add(GetBlock(att.Value));
							}
						}
					}
				}
			}
		}

		/// <summary>
		/// Parses the given XmlNode and its children for styles.
		/// </summary>
		/// <param name="node">XmlNode to parse.</param>
		private void ParseStyle(XmlNode node)
		{
			string name = "";

			string foreColor = "";
			string backColor = "";

			bool bold = false;
			bool italic = false;
			bool underline = false;

			foreach (XmlAttribute att in node.Attributes)
			{
				switch (att.Name.ToLower())
				{
					case "name":
						name = att.Value;
						break;
					case "forecolor":
						foreColor = att.Value;
						break;
					case "backcolor":
						backColor = att.Value;
						break;
					case "bold":
						bold = bool.Parse(att.Value);
						break;
					case "italic":
						italic = bool.Parse(att.Value);
						break;
					case "underline":
						underline = bool.Parse(att.Value);
						break;
				}
			}

			TextStyle textStyle = GetStyle(name);

			if (backColor != "")
				textStyle.BackColor = Color.FromName(backColor);

			if (foreColor.IndexOf(",") != -1)	// If the ForeColor string has "," in it,
			{									// it is an RGB color.
				foreColor = foreColor.Trim();
				string[] rgb = foreColor.Split(',');

				string sR = rgb[0];
				string sG = rgb[1];
				string sB = rgb[2];

				int r = Convert.ToInt32(sR);
				int g = Convert.ToInt32(sG);
				int b = Convert.ToInt32(sB);

				textStyle.ForeColor = Color.FromArgb(r, g, b);
			}
			else
				textStyle.ForeColor = Color.FromName(foreColor);

			textStyle.Bold = bold;
			textStyle.Italic = italic;
			textStyle.Underline = underline;
			textStyle.Name = name;
		}

		/// <summary>
		/// Returns the TextStyle representing the given name.
		/// </summary>
		/// <param name="name">Name to find TextStyle for.</param>
		/// <returns>The TextStyle representing the given name.</returns>
		private TextStyle GetStyle(string name)
		{
			if (styles[name] == null)
			{
				TextStyle textStyle = new TextStyle();
				styles.Add(name, textStyle);
			}

			return styles[name] as TextStyle;
		}

		/// <summary>
		/// Returns the block representing the given name.
		/// </summary>
		/// <param name="name">Name to find block for.</param>
		/// <returns>The block representing the given name.</returns>
		private Block GetBlock(string name)
		{
			if (blocks[name] == null)
			{
				Block block = new Block(language);
				blocks.Add(name, block);
			}

			return blocks[name] as Block;
		}

		#endregion

		#endregion

		/// <summary>
		/// Initializes a new instance of LanguageReader.
		/// </summary>
		public LanguageReader()
		{
		}
	}
}